package com.requestfactory.wire.spi.http;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.lang.SerializationUtils;

import com.requestfactory.wire.WireError;
import com.requestfactory.wire.WireRuntimeException;
import com.requestfactory.wire.spi.WireSPIRequest;
import com.requestfactory.wire.spi.WireSPIResponse;
import com.requestfactory.wire.spi.WireSPIStub;

public final class WireSPIStubApacheHTTPClientImpl implements WireSPIStub {
	
	private static final Logger logger = 
		Logger.getLogger(WireSPIStubApacheHTTPClientImpl.class.getName());
	
	private final String wireSkeletonURL;
	
	public WireSPIStubApacheHTTPClientImpl(String wireSkeletonURL) {
		assert wireSkeletonURL != null && !wireSkeletonURL.isEmpty();
		this.wireSkeletonURL = wireSkeletonURL;
	}

	@Override
	public Serializable executeMethod(
			final String className,
			final String methodName, 
			final Class<?>[] parameterTypes,
			final Serializable[] arguments,
			final Cipher cipher)
	throws Throwable {
		assert className != null  && !className.isEmpty();
		assert methodName != null && !methodName.isEmpty();
		assert arguments != null;
		assert cipher != null;
		//
		// invoke http server
		//
		HttpClient httpClient = new HttpClient();
		PostMethod postMethod = new PostMethod(wireSkeletonURL);
		postMethod.setRequestEntity(new RequestEntity() {
			public void writeRequest(OutputStream outputStream) 
		    throws IOException {
		    	assert outputStream != null;
		        SerializationUtils.serialize(
		        		new WireSPIRequest(
		        				className, methodName,
		        				parameterTypes, arguments), 
		        		new CipherOutputStream(outputStream, cipher));
		    }
			public boolean isRepeatable() {
		        return true;
		    }
		    public long getContentLength() {
		        return -1;
		    }
		    public String getContentType() {
		        return "application/binary";
		    }
		});
		WireSPIResponse response = null;
		boolean rethrow = false;
		try {
			httpClient.executeMethod(postMethod);
			if(logger.isLoggable(Level.INFO)) {
				logger.log(Level.INFO, 
						"WIRE-HTTP-RESPONSE statusCode=" + 
						postMethod.getStatusCode());
			}
			if (postMethod.getStatusCode() == HttpStatus.SC_OK) {
				try {
					response = (WireSPIResponse)SerializationUtils.deserialize(
							new CipherInputStream(
									postMethod.getResponseBodyAsStream(), 
									cipher));
				} catch(Throwable ex) {
					String message = "Cannot deserialize response";
					if(logger.isLoggable(Level.SEVERE)) {
						logger.log(Level.SEVERE, message);
					}
					throw new WireRuntimeException(
							WireError.WIRE_SPI_CANNOT_DESERIALIZE_RESPONSE,
							message);
				}
				if(response == null) {
					String message = "Response transfer object is null";
					if(logger.isLoggable(Level.SEVERE)) {
						logger.log(Level.SEVERE, message);
					}
					throw new WireRuntimeException(
							WireError.WIRE_SPI_FATAL_ERROR_NO_RESPONSE, 
							message);
				}
				assert response != null;
				if(response.getValue() instanceof Throwable) {
					if(response.getValue() instanceof WireRuntimeException) {
						logger.log(Level.SEVERE, 
								"WIRE-HTTP-RESPONSE, remote HTTP POST '" +
								wireSkeletonURL + "' failed with internal error", 
								(Throwable)response.getValue());
					}
					if(response.getValue() instanceof InvocationTargetException) {
						Throwable targetException =
							((InvocationTargetException)response.getValue()).getTargetException();
						if(targetException instanceof HttpException ||
								targetException instanceof IOException) {
							rethrow = true;
						}
						throw targetException;
					}
					throw (Throwable)response.getValue();
				}
			} else {
				String message = 
					"Remote invokation failed with HTTP POST status code " +
					postMethod.getStatusCode() + " " +
					postMethod.getStatusLine().toString();
				if(logger.isLoggable(Level.SEVERE)) {
					logger.log(Level.SEVERE, message);
				}
				throw new WireRuntimeException(
						WireError.WIRE_SPI_FATAL_ERROR_INVOKATION_FAILED,
						message);
			}
		} catch(HttpException ex) {
			if(rethrow) throw ex;
			if(logger.isLoggable(Level.SEVERE)) {
				logger.log(Level.SEVERE, ex.getMessage(), ex);
			}
			throw new WireRuntimeException(
					WireError.WIRE_SPI_HTTP_ERROR, ex.getMessage(), ex);
		} catch(IOException ex) {
			if(rethrow) throw ex;
			if(logger.isLoggable(Level.SEVERE)) {
				logger.log(Level.SEVERE, ex.getMessage(), ex); 
			}
			throw new WireRuntimeException(
					WireError.WIRE_SPI_IO_ERROR, ex.getMessage(), ex);
		} finally {
			postMethod.releaseConnection();
		}
		return response.getValue();
	}

}
